iT邦幫忙

2023 iThome 鐵人賽

DAY 12
0
Modern Web

前端開發之那些我會的與我不會的技術系列 第 12

React 中的 useState:狀態快照

  • 分享至 

  • xImage
  •  

渲染細節流程

前幾篇我們有提到useState的setter function會觸發重新渲染這個機制,這篇我們會更詳細說明渲染的細節。

首先們先回顧一下整個更新畫面的步驟

  1. Triggering a render
  2. Rendering component
  3. Committing to the DOM

在呼叫setter function後,不是直接觸發重新渲染,也不會馬上更新狀態值,而是將setter要執行的運算加入到queue,待event handler其他程式執行完再批次執行queue與使用新的狀態渲染,最後完成畫面更新。每次渲染完JSX都會產生一個快照(snapshot),這個快照會有當下的狀態、事件綁定,呈現的UI會根據這次的狀態完成更新。

以下看個範例會更容易理解

import { useState } from "react";

function App() {
  const [val, setVal] = useState(0);
  return (
    <>
      <h2 id="val">{val}</h2>
      <button id="btn" onClick={() => {
				setVal(val + 1);
				setVal(val + 1);
				setVal(val + 1);
			}}>+3</button>
    </>
  )
}

export default App

當你點擊1次+3時,結果會是1不是3,因為狀態會根據快照的值去運算且不會即時更新。

因為val的初始值是0,所以setVal實際上是

setVal(0 + 1)

setVal(0 + 1)

setVal(0 + 1)

根據上述val的結果會是1不會是3,然後React會將這個結果1交給下個渲染產生另個快照。

如果想要結果是3的話可以使用帶入function的方式,並使用預設function帶入的參數-前一個操作執行的結果

import { useState } from "react";

function App() {
  const [val, setVal] = useState(0);
  return (
    <>
      <h2 id="val">{val}</h2>
      <button id="btn" onClick={() => {
				setVal(preVal => preVal + 1);
				setVal(preVal => preVal + 1);
				setVal(preVal => preVal + 1);
			}}>+3</button>
    </>
  )
}

export default App

這次點擊+3的結果就會是3,實際上的執行如下

setVal(0 => 0 + 1);

setVal(1 => 1 + 1);

setVal(2 => 2 + 1);

所以這次的結果就會是3。

import { useState } from "react";

function App() {
  const [val, setVal] = useState(0);
  return (
    <>
      <h2 id="val">{val}</h2>
      <button id="btn" onClick={() => {
				setVal(preVal => preVal + 1);
				setVal(preVal => preVal + 1);
				setVal(preVal => preVal + 1);
				alert(val);
			}}>+3</button>
    </>
  )
}

export default App

我們可以用個alert來驗證val的值,結果會是原始快照的0,並且是先跳出alert後才渲染並更新畫面。

再來另個例子,如果在非同步的情況也會有什麼樣的結果

import { useState } from "react";

function App() {
  const [val, setVal] = useState(0);
  return (
    <>
      <h2 id="val">{val}</h2>
      <button id="btn" onClick={() => {
				setVal(preVal => preVal + 1);
				setVal(preVal => preVal + 1);
				setVal(preVal => preVal + 1);
        setTimeout(() => {
          alert(val);          
        }, 3000);
			}}>+3</button>
    </>
  )
}

export default App

alert經過3秒後一樣是跳出0。

這結果跟我們在JS上的不太一樣,如果是在JS結果會是3

let v = 0;
v = v + 1;
v = v + 1;
v = v + 1;

setTimeout(() => {
  alert(v);          
}, 3000);

這是因為在React不管是不是同步還非同步,都會保留原本快照的狀態進行運算。

這邊還有個官方文件提出的範例,在alert出現前使用者更動alert的值,我們來看看結果

和之前的結果一樣,會咬住一開始快照的狀態,不會因為操作而改變。

更新步驟

我們在一開始有提到在觸發事件後執行的順序,以這個範例為例

import { useState } from "react";

function App() {
  const [val, setVal] = useState(0);
function clickHandler() {
    setVal(n => {
      console.log(1);
      return n + 1;
    })
    setVal(n => {
      console.log(2);
      return n + 1;
    })
    setVal(n => {
      console.log(3);
      return n + 1;
    })
    console.log('other action')
  }
  return (
    <>
      <h2 id="val">{val}</h2>
      <button id="btn" onClick={clickHandler}>+3</button>
    </>
  )
}

export default App

使用者操作觸發event handler→將setVal放到queue→執行alert→執行queue裡的程式→渲染→產生virtual DOM並與上一版的比較(這邊可以先忽略之後會再介紹)→更新畫面

我在第一次執行點擊的時候不會是這個流程?但是之後就會依照這個文件上的流程,為什麼會有這樣的結果?歡迎跟我分享原因

https://ithelp.ithome.com.tw/upload/images/20230927/20162751Ay3u22AYtL.png

參考

https://react.dev/learn/state-as-a-snapshot


上一篇
React 中的 useState:狀態管理和重新渲染的關鍵
下一篇
了解React渲染和畫面更新
系列文
前端開發之那些我會的與我不會的技術31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言